﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using uTILLIty.Rave.Contract.Configuration;
using uTILLIty.Rave.Contract.StorageAdapter;
using uTILLIty.Rave.Core.Manager;
using Cfg = uTILLIty.Rave.Contract.Configuration;
using Directory = uTILLIty.Rave.Contract.Configuration.Directory;

namespace uTILLIty.Rave.Core.Configuration
{
	/// <summary>
	/// Extensions for the <see cref="ConfigurationManager"/> to the <see cref="uTILLIty.Rave.Contract.Configuration.Configuration"/> class
	/// </summary>
	public static class ConfigurationExtensions
	{

		/// <summary>
		/// Serializes the supplied object to XML
		/// </summary>
		public static string ToXml<T>(this T source, Encoding encoding, Formatting formatting)
		{
			using (var stream = new MemoryStream())
			{
				using (var writer = new XmlTextWriter(stream, encoding))
				{
					writer.Formatting = formatting;
					var serializer = new XmlSerializer(typeof(T));
					serializer.Serialize(writer, source);
					writer.Flush();
					stream.Seek(0, SeekOrigin.Begin);
					var output = new StreamReader(stream, encoding).ReadToEnd();
					return output;
				}
			}
		}

		/// <summary>
		/// Creates a new Configuration object by deserializing the <paramref name="configData"/> supplied
		/// </summary>
		/// <param name="config">ignored - can be null</param>
		/// <param name="configData">a stream representing the XML file to deserialize</param>
		/// <param name="schemas">an optional list of schemas to use for validating the <paramref name="configData"/></param>
		/// <returns>A new instance of the <see cref="Configuration"/></returns>
		public static Cfg.Configuration Load(this Cfg.Configuration config, Stream configData, XmlSchemaSet schemas)
		{
			if (configData == null)
				throw new ArgumentNullException("configData");

			var settings = new XmlReaderSettings { ValidationType = ValidationType.None };
			if (schemas != null)
			{
				settings.ValidationEventHandler += OnValidationEvent;
				settings.ValidationType = ValidationType.Schema;
				settings.Schemas.Add(schemas);
			}

			var serializer = new XmlSerializer(typeof(Cfg.Configuration));
			using (var reader = XmlReader.Create(configData, settings))
			{
				var output = (Cfg.Configuration)serializer.Deserialize(reader);
				return output;
			}
		}

		private static void OnValidationEvent(object sender, ValidationEventArgs e)
		{
			switch (e.Severity)
			{
				case XmlSeverityType.Error:
					throw new XmlSchemaValidationException(e.Message, e.Exception);
			}
		}

		/// <summary>
		/// Traverses all directories of the <paramref name="config">configuration</paramref> supplied and
		/// invokes the supplied <paramref name="visitor"/>
		/// </summary>
		/// <remarks>
		/// This implementation hierarchically traverses the tree without deepening the call-stack
		/// </remarks>
		/// <param name="config">the configuration to traverse</param>
		/// <param name="visitor">the visitor to invoke for each directory</param>
		/// <param name="errorHandler">an optional exception-handler. if none is supplied, the exception is thrown instead</param>
		public static void VisitDirectories(this Cfg.Configuration config, Action<VisitingContext> visitor, Action<VisitingError> errorHandler)
		{
			if (visitor == null)
				throw new ArgumentNullException("visitor");

			var directories = new Queue<DirectoryContext>(10);
			var rootDirInfo = new DirectoryContext(new Directory { Name = config.RootDirectory }, null);
			foreach (var dirConfig in config.Directories)
				directories.Enqueue(new DirectoryContext(dirConfig, rootDirInfo));

			while (directories.Count > 0)
			{
				var currentDirInfo = directories.Dequeue();

				bool visitChildren;
				try
				{
					var vc = new VisitingContext(currentDirInfo);
					visitor(vc);
					visitChildren = vc.VisitChildren;
				}
				catch (Exception ex)
				{
					if (errorHandler == null)
						throw;

					var eh = new VisitingError(currentDirInfo, ex);
					errorHandler(eh);
					if (eh.Rethrow)
						throw;

					visitChildren = eh.VisitChildren;
				}

				if (!visitChildren || currentDirInfo.Directory.SubDirectories == null)
					continue;

				foreach (var dirConfig in currentDirInfo.Directory.SubDirectories.Where(sd => !string.IsNullOrWhiteSpace(sd.Name)))
					directories.Enqueue(new DirectoryContext(dirConfig, currentDirInfo));
			}
		}

		/// <summary>
		/// A context provided when <see cref="ConfigurationExtensions.VisitDirectories"/> is called
		/// </summary>
		public class VisitingContext
		{
			internal VisitingContext(DirectoryContext directoryContext)
			{
				DirectoryContext = directoryContext;
				VisitChildren = true;
			}

			/// <summary>
			/// The directory to visit
			/// </summary>
			public DirectoryContext DirectoryContext { get; set; }

			/// <summary>
			/// Set by the visitor to indicate to the calling method (<see cref="ConfigurationExtensions.VisitDirectories"/>)
			/// not to traverse this <see cref="DirectoryContext"/>s child-directories
			/// </summary>
			public bool VisitChildren { get; set; }
		}

		/// <summary>
		/// The error-context provided to the error-handler when an error has occured visiting the directories
		/// </summary>
		/// <seealso cref="ConfigurationExtensions.VisitDirectories"/>
		public class VisitingError
		{
			internal VisitingError(DirectoryContext directoryContext, Exception exception)
			{
				DirectoryContext = directoryContext;
				Exception = exception;
			}

			/// <summary>
			/// The <see cref="DirectoryContext">directory</see> which was visited when the error occured
			/// </summary>
			public DirectoryContext DirectoryContext { get; protected set; }

			/// <summary>
			/// Wether to visit the <see cref="DirectoryContext">directories</see> child-directories
			/// </summary>
			public bool VisitChildren { get; set; }

			/// <summary>
			/// The unhandled exception causing the interuption
			/// </summary>
			public Exception Exception { get; protected set; }

			/// <summary>
			/// Wether the calling code should re-throw the original <see cref="Exception"/>
			/// </summary>
			public bool Rethrow { get; set; }
		}
	}
}